//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Text;
using EffiProzDB.Store;
using EffiProzDB.Lib;
using EffiProzDB.RowIO;
using EffiProzDB.ScriptIO;

namespace EffiProzDB.Persist
{
    // Ito Kazumitsu 20030328 - patch 1.7.2 - character encoding support
    // Dimitri Maziuk - patch for NL _in string support
    // sqlbob@users - updated for 1.8.0 to allow new-lines _in fields
    // fredt@users - updated for 1.8.0 to allow correct behaviour with transactions

    /**
     * Acts as a buffer manager for a single TEXT table with respect its Row data.<p>
     *
     * Handles read/write operations on the table's text format data file using a
     * compatible pair of org.hsqldb.rowio input/output class instances.
     *
     *
     * @author sqlbob@users (RMP)
     * @version 1.8.0
     * @since 1.7.0
     */
    public class TextCache : DataFileCache
    {

        //state of Cache
        public static string NL = Environment.NewLine; //.GetEnvironmentVariable("line.separator");
            public string              fs;
            public string              vs;
            public string              lvs;
            public string              stringEncoding;
            public bool             isQuoted;
            public bool             isAllQuoted;
            public bool             ignoreFirst;
            protected string           header;
            protected Table            table;
            private ObjectCacheHashMap uncommittedCache;

            //
            const  char DOUBLE_QUOTE_CHAR = '\"';
            const  char BACKSLASH_CHAR    = '\\';
            const  char LF_CHAR           = '\n';
            const  char CR_CHAR           = '\r';

            /**
             *  The source string for a cached table is evaluated and the parameters
             *  are used to open the source file.<p>
             *
             *  Settings are used _in this order: (1) settings specified _in the
             *  source string for the table (2) global database settings in
             *  *.properties file (3) program defaults
             *
             *  fredt - this used to write rows as soon as they are inserted
             *  but now this is subject to session autoCommit / or commit
             *  storeOnInsert = true;
             */
            public TextCache(Table table, string name)  : base(table.database, name) {



                this.table       = table;
                uncommittedCache = new ObjectCacheHashMap(5);
            }

            protected override void initParams(Database database,
                                      string baseFileName)   {

                fileName      = baseFileName;
                this.database = database;
                fa            = FileUtil.GetDefaultInstance();

                HsqlProperties tableprops =
                    HsqlProperties.delimitedArgPairsToProps(fileName, "=", ";", null);

                //-- Get file name
                switch (tableprops.errorCodes.Length) {

                    case 0 :
                        throw Trace.error(Trace.TEXT_TABLE_SOURCE,
                                          Trace.TEXT_TABLE_SOURCE_FILENAME);
                    case 1 :

                        // source file name is the only key without a value
                        fileName = tableprops.errorKeys[0].Trim();
                        break;

                    default :
                        throw Trace.error(Trace.TEXT_TABLE_SOURCE,
                                          Trace.TEXT_TABLE_SOURCE_VALUE_MISSING,
                                          tableprops.errorKeys[1]);
                }

                //-- Get separators:
                HsqlDatabaseProperties dbProps = database.getProperties();

                fs = translateSep(tableprops.getProperty("fs",
                        dbProps.getProperty(HsqlDatabaseProperties.textdb_fs, ",")));
                vs = translateSep(tableprops.getProperty("vs",
                        dbProps.getProperty(HsqlDatabaseProperties.textdb_vs, fs)));
                lvs = translateSep(tableprops.getProperty("lvs",
                        dbProps.getProperty(HsqlDatabaseProperties.textdb_lvs, fs)));

                if (fs.Length  == 0 || vs.Length  == 0 || lvs.Length  == 0) {
                    throw Trace.error(Trace.TEXT_TABLE_SOURCE,
                                      Trace.TEXT_TABLE_SOURCE_SEPARATOR);
                }

                //-- Get bools
                ignoreFirst = tableprops.isPropertyTrue(
                    "ignore_first",
                    dbProps.isPropertyTrue(
                        HsqlDatabaseProperties.textdb_ignore_first, false));
                isQuoted = tableprops.isPropertyTrue(
                    "quoted",
                    dbProps.isPropertyTrue(
                        HsqlDatabaseProperties.textdb_quoted, true));
                isAllQuoted = tableprops.isPropertyTrue(
                    "all_quoted",
                    dbProps.isPropertyTrue(
                        HsqlDatabaseProperties.textdb_all_quoted, false));

                //-- Get encoding
                stringEncoding = translateSep(tableprops.getProperty("encoding",
                        dbProps.getProperty(HsqlDatabaseProperties.textdb_encoding,
                                            "ASCII")));

                //-- Get size and scale
                int cacheScale = tableprops.getIntegerProperty(
                    "cache_scale",
                    dbProps.getIntegerProperty(
                        HsqlDatabaseProperties.textdb_cache_scale, 10, 8, 16));
                int cacheSizeScale = tableprops.getIntegerProperty(
                    "cache_size_scale",
                    dbProps.getIntegerProperty(
                        HsqlDatabaseProperties.textdb_cache_size_scale, 10, 8, 20));
                int lookupTableLength = 1 << cacheScale;
                int avgRowBytes       = 1 << cacheSizeScale;

                maxCacheSize     = lookupTableLength * 3;
                maxCacheBytes    = maxCacheSize * avgRowBytes;
                maxDataFileSize  = int.MaxValue;
                cachedRowPadding = 1;
                cacheFileScale   = 1;
            }

            protected override void initBuffers() {

                if (isQuoted || isAllQuoted) {
                    rowIn = new RowInputTextQuoted(fs, vs, lvs, isAllQuoted);
                    rowOut = new RowOutputTextQuoted(fs, vs, lvs, isAllQuoted,
                                                     stringEncoding);
                } else {
                    rowIn  = new RowInputText(fs, vs, lvs, false);
                    rowOut = new RowOutputText(fs, vs, lvs, false, stringEncoding);
                }
            }

            private string translateSep(string sep) {
                return translateSep(sep, false);
            }

            /**
             * Translates the escaped characters _in a separator string and returns
             * the non-escaped string.
             */
            private string translateSep(string sep, bool isProperty) {

                if (sep == null) {
                    return (null);
                }

                int next = 0;

                if ((next = sep.IndexOf(BACKSLASH_CHAR)) != -1) {
                    int          start    = 0;
                    char[]       sepArray = sep.ToCharArray();
                    char         ch       =(char) 0;
                    int          len      = sep.Length ;
                    StringBuilder realSep  = new StringBuilder(len);

                    do {
                        realSep.Append(sepArray, start, next - start);

                        start = ++next;

                        if (next >= len) {
                            realSep.Append(BACKSLASH_CHAR);

                            break;
                        }

                        if (!isProperty) {
                            ch = sepArray[next];
                        }

                        if (ch == 'n') {
                            realSep.Append(LF_CHAR);

                            start++;
                        } else if (ch == 'r') {
                            realSep.Append(CR_CHAR);

                            start++;
                        } else if (ch == 't') {
                            realSep.Append('\t');

                            start++;
                        } else if (ch == BACKSLASH_CHAR) {
                            realSep.Append(BACKSLASH_CHAR);

                            start++;
                        } else if (ch == 'u') {
                            start++;

                            realSep.Append(
                                (char) int.Parse(
                                    sep.Substring( start ,  start + 4 - (start) ), System.Globalization.NumberStyles.HexNumber));

                            start += 4;
                        } else if (sep.Substring(next).StartsWith("semi")) {
                            realSep.Append(';');

                            start += 4;
                        }
                        else if (sep.Substring(next).StartsWith("space"))
                        {
                            realSep.Append(' ');

                            start += 5;
                        }
                        else if (sep.Substring(next).StartsWith("quote"))
                        {
                            realSep.Append(DOUBLE_QUOTE_CHAR);

                            start += 5;
                        }
                        else if (sep.Substring(next).StartsWith("apos"))
                        {
                            realSep.Append('\'');

                            start += 4;
                        } else {
                            realSep.Append(BACKSLASH_CHAR);
                            realSep.Append(sepArray[next]);

                            start++;
                        }
                    } while ((next = sep.IndexOf(BACKSLASH_CHAR, start)) != -1);

                    realSep.Append(sepArray, start, len - start);

                    sep = realSep.ToString();
                }

                return sep;
            }

            /**
             *  Opens a data source file.
             */
            public override void open(bool _readonly)   {

                fileFreePosition = 0;

                try {
                    dataFile = ScaledRAFile.newScaledRAFile(database, fileName,
                            _readonly, ScaledRAFile.DATA_FILE_RAF);
                    fileFreePosition = dataFile.length() ;

                    if (fileFreePosition > int.MaxValue) {
                        throw new System.IO.IOException();
                    }

                    initBuffers();
                } catch (Exception e) {
                    throw Trace.error(Trace.FILE_IO_ERROR,
                                      Trace.TextCache_openning_file_error,
                                      new Object[] {
                        fileName, e
                    });
                }

                cacheReadonly = _readonly;
            }

           public  void reopen()   {
                open(cacheReadonly);
            }

            /**
             *  Writes newly created rows to disk. In the current implentation,
             *  such rows have already been saved, so this method just removes a
             *  source file that has no rows.
             */
            public override void close(bool write)   {

                if (dataFile == null) {
                    return;
                }

                try {
                    cache.saveAll();

                    bool empty = (dataFile.length()  <= NL.Length );

                    dataFile.close();

                    dataFile = null;

                    if (empty && !cacheReadonly) {
                        FileUtil.GetDefaultInstance().delete(fileName);
                    }
                } catch (Exception e) {
                    throw Trace.error(Trace.FILE_IO_ERROR,
                                      Trace.TextCache_closing_file_error,
                                      new Object[] {
                        fileName, e
                    });
                }
            }

            /**
             * Closes the source file and deletes it if it is not read-only.
             */
            public void purge()   {

                uncommittedCache.clear();

                try {
                    if (cacheReadonly) {
                        close(false);
                    } else {
                        if (dataFile != null) {
                            dataFile.close();

                            dataFile = null;
                        }

                        FileUtil.GetDefaultInstance().delete(fileName);
                    }
                } catch (Exception e) {
                    throw Trace.error(Trace.FILE_IO_ERROR,
                                      Trace.TextCache_purging_file_error,
                                      new Object[] {
                        fileName, e
                    });
                }
            }

            /**
             *
             */
            public override void remove(int pos,
                                            PersistentStore store)   {
                lock (this)
                {
                    CachedObject row = (CachedObject)uncommittedCache.remove(pos);

                    if (row != null)
                    {
                        return;
                    }

                    row = cache.release(pos);

                    clearRowImage(row);
                    release(pos);
                }
            }

            private void clearRowImage(CachedObject row)   {

                int length = row.getStorageSize()
                             - ScriptWriterText.BYTES_LINE_SEP.Length;

                rowOut.reset();

                HsqlByteArrayOutputStream _out = rowOut.getOutputStream();

                _out.fill(' ', length);
                _out.write(ScriptWriterText.BYTES_LINE_SEP);
                dataFile.seek(row.getPos());
                dataFile.write(_out.getBuffer(), 0, _out.size());
            }

            public override void removePersistence(int pos,
                PersistentStore store)   {
                    lock (this)
                    {
                        CachedObject row = (CachedObject)uncommittedCache.get(pos);

                        if (row != null)
                        {
                            return;
                        }

                        row = cache.get(pos);

                        clearRowImage(row);
                    }
            }

            public override RowInputInterface readObject(int pos)
              {
                  lock (this)
                  {
                      ByteArray buffer = new ByteArray(this,80);
                      bool complete = false;
                      bool wasCR = false;
                      int c;
                      bool hasQuote = false;
                      bool wasNormal = false;

                      pos = findNextUsedLinePos(pos);

                      if (pos == -1)
                      {
                          return null;
                      }

                      dataFile.seek(pos);

                      while (!complete)
                      {
                          wasNormal = false;
                          c = dataFile.read();

                          if (c == -1)
                          {
                              if (buffer.length() == 0)
                              {
                                  return null;
                              }

                              complete = true;

                              if (wasCR)
                              {
                                  break;
                              }

                              if (!cacheReadonly)
                              {
                                  dataFile.write(ScriptWriterText.BYTES_LINE_SEP, 0,
                                                 ScriptWriterText.BYTES_LINE_SEP.Length);
                              }

                              break;
                          }

                          switch (c)
                          {

                              case DOUBLE_QUOTE_CHAR:
                                  wasNormal = true;
                                  complete = wasCR;
                                  wasCR = false;

                                  if (isQuoted)
                                  {
                                      hasQuote = !hasQuote;
                                  }
                                  break;

                              case CR_CHAR:
                                  wasCR = !hasQuote;
                                  break;

                              case LF_CHAR:
                                  complete = !hasQuote;
                                  break;

                              default:
                                  wasNormal = true;
                                  complete = wasCR;
                                  wasCR = false;
                                  break;
                          }

                          buffer.append(c);
                      }

                      if (complete)
                      {
                          int length = (int)dataFile.getFilePointer() - pos;

                          if (wasNormal)
                          {
                              length--;
                          }

                          ((RowInputText)rowIn).setSource(buffer.ToString(), pos, length);

                          return rowIn;
                      }

                      return null;
                  }
            }

            public int readHeaderLine()   {

                bool   complete  = false;
                bool   wasCR     = false;
                bool   wasNormal = false;
                ByteArray buffer    = new ByteArray(this,80);

                while (!complete) {
                    wasNormal = false;

                    int c;

                    try {
                        c = dataFile.read();

                        if (c == -1) {
                            if (buffer.length()  == 0) {
                                return 0;
                            }

                            complete = true;

                            if (!cacheReadonly) {
                                dataFile.write(ScriptWriterText.BYTES_LINE_SEP, 0,
                                               ScriptWriterText.BYTES_LINE_SEP.Length);
                            }

                            break;
                        }
                    } catch (Exception ) {
                        throw Trace.error(Trace.TEXT_FILE);
                    }

                    switch (c) {

                        case CR_CHAR :
                            wasCR = true;
                            break;

                        case LF_CHAR :
                            complete = true;
                            break;

                        default :
                            wasNormal = true;
                            complete  = wasCR;
                            wasCR     = false;
                            break;
                    }

                    buffer.append(c);
                }

                header = buffer.ToString();

                try {
                    int length = (int) dataFile.getFilePointer();

                    if (wasNormal) {
                        length--;
                    }

                    return length;
                } catch (Exception ) {
                    throw Trace.error(Trace.TEXT_FILE);
                }
            }

            // fredt - new method

            /**
             * Searches from file pointer, pos, and finds the beginning of the first
             * line that contains any non-space character. Increments the row counter
             * when a blank line is skipped.
             *
             * If none found return -1
             */
           public int findNextUsedLinePos(int pos)   {

                int     firstPos   = pos;
                int     currentPos = pos;
                bool wasCR      = false;

                dataFile.seek(pos);

                while (true) {
                    int c = dataFile.read();

                    currentPos++;

                    switch (c) {

                        case CR_CHAR :
                            wasCR = true;
                            break;

                        case LF_CHAR :
                            wasCR = false;

                            ((RowInputText) rowIn).skippedLine();

                            firstPos = currentPos;
                            break;

                        case ' ' :
                            if (wasCR) {
                                wasCR = false;

                                ((RowInputText) rowIn).skippedLine();
                            }
                            break;

                        case -1 :
                            return -1;

                        default :
                            return firstPos;
                    }
                }
            }

            public override  void add(CachedObject _object)   {
                lock (this)
                {
                    base.add(_object);
                    clearRowImage(_object);
                }
            }

            public override  CachedObject get(int i, PersistentStore store,
                                                 bool keep)   {
                 lock (this)
                 {
                     if (i < 0)
                     {
                         return null;
                     }

                     CachedObject o = (CachedObject)uncommittedCache.get(i);

                     if (o == null)
                     {
                         o = base.get(i, store, keep);
                     }

                     /*
                             if (o == null) {
                                 o = base.get(i, store, keep);
                             }
                     */
                     return o;
                }
            }

            /**
             * This is called internally when old rows need to be removed from the
             * cache. Text table rows that have not been saved are those that have not
             * been committed yet. So we don't save them but add them to the
             * uncommitted cache until such time that they are committed or rolled
             * back- fredt
             */
            public override void saveRows(CachedObject[] rows, int offset,
                                                 int count)   {
                 lock (this)
                 {
                     if (count == 0)
                     {
                         return;
                     }

                     for (int i = offset; i < offset + count; i++)
                     {
                         CachedObject r = rows[i];

                         uncommittedCache.put(r.getPos(), r);

                         rows[i] = null;
                     }
                 }
            }

            /**
             * In case the row has been moved to the uncommittedCache, removes it.
             * Then saves the row as normal.
             */
            public override  void saveRow(CachedObject row)   {
                lock (this)
                {
                    uncommittedCache.remove(row.getPos());
                    base.saveRow(row);
                }
            }

            public string getHeader() {
                return header;
            }

            public void setHeader(string header)   {

                if (ignoreFirst && fileFreePosition == 0) {
                    try {
                        writeHeader(header);

                        this.header = header;
                    } catch (Exception e) {
                        throw new HsqlException(
                            e, Trace.getMessage(Trace.GENERAL_IO_ERROR),
                            Trace.GENERAL_IO_ERROR);
                    }

                    return;
                }

                throw Trace.error(Trace.TEXT_TABLE_HEADER);
            }

            private void writeHeader(string header)   {

                byte[] buf       = null;
                string firstLine = header + NL;

                try {
                    buf = Encoding.GetEncoding(stringEncoding).GetBytes( firstLine);
                } catch (Exception) {
                    buf = Encoding.Default.GetBytes(firstLine);
                }

                dataFile.write(buf, 0, buf.Length);

                fileFreePosition = buf.Length;
            }

          public class ByteArray {

                private byte[] buffer;
                private int    buflen;
                private TextCache _o;

                public ByteArray(TextCache _o, int n) {
                    buffer = new byte[n];
                    buflen = 0;
                    this._o = _o;
                }

                public void append(int c) {

                    if (buflen >= buffer.Length) {
                        byte[] newbuf = new byte[buflen + 80];

                        Array.Copy(buffer, 0, newbuf, 0, buflen);

                        buffer = newbuf;
                    }

                    buffer[buflen] = (byte) c;

                    buflen++;
                }

                public int length() {
                    return buflen;
                }

                public void setLength(int l) {
                    buflen = l;
                }

                public override string ToString() {

                    try {
                        return Encoding.GetEncoding( _o.stringEncoding).GetString(buffer, 0, buflen );
                    } catch (Exception ) {
                        return Encoding.Default.GetString(buffer, 0, buflen);
                    }
                }
            }

            public int getLineNumber() {
                return ((RowInputText) rowIn).getLineNumber();
            }

            protected override void setFileModified()   {
                fileModified = true;
            }
    }
}
